//    Copyright (C) Kherty.  All rights reserved.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using OpenLS.Spreadsheet.Extensibility;

namespace OpenLS.Spreadsheet.StandardAddIn
{
    [AddInFunctions]
    public static class MathFunctions
    {
        public static double CosH([TreatNullsAsZero] double number)
        {
            return Math.Cosh(number);
        }


        public static double Abs([TreatNullsAsZero] double number)
        {
            return Math.Abs(number);
        }

        public static double Acos([TreatNullsAsZero] double v)
        {
            return Math.Acos(v);
        }

        public static double Asin([TreatNullsAsZero] double v)
        {
            return Math.Asin(v);
        }

        public static double Asinh([TreatNullsAsZero] double number)
        {
            return Math.Log(number + Math.Sqrt(number*number + 1));
        }

        public static double Acosh([TreatNullsAsZero] double number)
        {
            return Math.Log(number + Math.Sqrt(number*number - 1));
        }

        public static double Atan([TreatNullsAsZero] double value)
        {
            return Math.Atan(value);
        }

        public static double Atan2([TreatNullsAsZero] double x_num, [TreatNullsAsZero] double y_num)
        {
            return Math.Atan2(x_num, y_num);
        }

        public static double Atanh([TreatNullsAsZero] double number)
        {
            return (1d/2d)*Math.Log((1 + number)/(1 - number));
        }

        public static double Combin([TreatNullsAsZero] double number, [TreatNullsAsZero] double number_chosen)
        {
            return Fact(number)/Fact(number_chosen)/Fact(number - number_chosen);
        }

        public static double Degrees([TreatNullsAsZero] double number)
        {
            return number*180/Math.PI;
        }

        public static double Even([TreatNullsAsZero] double number)
        {
            if (number < 0)
                return -Even(-number);
            return Math.Ceiling(number/2)*2;
        }

        public static double Exp([TreatNullsAsZero] double number)
        {
            return Math.Exp(number);
        }

        public static double Fact([TreatNullsAsZero] double number)
        {
            int i = (int) number;
            if (i < 0)
                throw new ErrorException(Errors.NumError);

            if (i == 1)
                return 1;
            if (i == 0)
                return 1;
            return Fact(i - 1)*i;
        }

        /*private static double FactDouble([TreatNullsAsZero] double number)
        {
            int i = (int) number;
            if (i < 0)
                throw new NotSupportedException();
            return Fact(i);
        }*/
        [AddInFunction(Name="FLOOR.PRECISE")]
        public static object Floor_Precise([TreatNullsAsZero] double number, [Optional (1d),TreatNullsAsZero] double significance)
        {
            return FloorImp(number, Math.Abs(significance));
        }

        public static object Floor([TreatNullsAsZero] double number, [TreatNullsAsZero] double significance)
        {
            if (number >0 && significance < 0)
                return Errors.NumError;
            return FloorImp(number, significance);
        }
        static object FloorImp([TreatNullsAsZero] double number, [TreatNullsAsZero] double significance)
        {
            if (significance < 0)
                return CeilingImp(number, -significance);
            if (significance == 0)
                if (number == 0)
                    return 0;
                else
                    return Errors.DivError;
            double d = number/significance;
            return significance * Math.Floor(d);
        }
        [AddInFunction(Name = "ISO.CEILING")]
        public static object Iso_Ceiling([TreatNullsAsZero] double number, [Optional (1d),TreatNullsAsZero] double significance)
        {
            return CeilingImp(number, Math.Abs(significance));
        }

        public static object Ceiling([TreatNullsAsZero] double number, [TreatNullsAsZero] double significance)
        {
            if (number > 0 && significance < 0)
                return Errors.NumError;
            return CeilingImp(number, significance);
        }


        static object CeilingImp([TreatNullsAsZero] double number, [TreatNullsAsZero] double significance)
        {
            if (significance < 0)
                return FloorImp(number, -significance);
            if (significance == 0)
                if (number == 0)
                    return 0;
                else
                    return Errors.DivError;
            double d = number/significance;

            return significance * Math.Ceiling(d);
        }


        public static int Int([TreatNullsAsZero] double number)
        {
            return (int) Math.Floor(number);
        }

        public static object Ln([TreatNullsAsZero] double number)
        {
            if (number <= 0)
                return Errors.NullError;
            return Math.Log(number);
        }

        public static double Log([TreatNullsAsZero] double number, [Optional(10d)] double @base)
        {
            return Math.Log(number, @base);
        }

        public static double Log10([TreatNullsAsZero] double number)
        {
            return Math.Log10(number);
        }

        public static double MDeterm(IArray array)
        {
            Matrix m = new Matrix(array);
            return m.Determinant;
        }

        public static IArray MInverse(IArray array )
        {
            Matrix m = new Matrix(array);
            m = m.Inverse;
            return new FuncArray((row, column) => m[row, column], m.RowCount, m.ColumnCount);
        }


        public static IArray MMult(IArray array1, IArray array2)
        {
            Matrix m = new Matrix(array1) * new Matrix(array2);
            return new FuncArray((row, column) => m[row, column], m.RowCount, m.ColumnCount);
        }

        public static double Mod(double number, double divisor)
        {
            var result =  Math.IEEERemainder(number, divisor);
            if (divisor > 0)
            {
                if (result < 0)
                    result += divisor;
            }
            if (divisor < 0)
            {
                if (result > 0)
                    result += divisor;
            }
            return result;
        }

        public static double Odd([TreatNullsAsZero] double number)
        {
            if (number < 0)
                return -Odd(-number);
            double d = (number + 1)/2;
            if (d == (int) d)
                return number;
            return 1 + 2*Math.Floor((number + 1)/2);
        }

        public static double Pi()
        {
            return Math.PI;
        }

        public static double Power([TreatNullsAsZero] double number1, [TreatNullsAsZero] double number2)
        {
            return Math.Pow(number1, number2);
        }

        public static double Product(
            [IgnoreBooleans, IgnoreNulls, AllowStringToDoubleConversion(ReferenceOption.ArgumentOnly, Strict = true),
             IgnoreStrings(ReferenceOption.ReferenceOnly)] params double[] numbers)
        {
            if (numbers.Length == 0)
                return 0;
            double result = 1;
            foreach (double number in numbers)
                result *= number;
            return result;
        }

        public static double Radians([TreatNullsAsZero] double number)
        {
            return number/180*Math.PI;
        }

        public static double Rand()
        {
            return new Random(Guid.NewGuid().GetHashCode()).NextDouble();
        }

        public static string Roman([TreatErrorsAsZero] double number, [Optional(0)] int form)
        {
            int n = (int) number;
            if (n <= 0 || n >= 4000)
                throw new ArgumentOutOfRangeException("number");
            if (form < 0 || form >= 5)
                throw new ArgumentOutOfRangeException("form");
            int j;
            int dec = 1000;
            StringBuilder result = new StringBuilder();
            char[] letter = {'M', 'D', 'C', 'L', 'X', 'V', 'I'};

            for (j = 0; dec > 1; dec /= 10, j += 2)
            {
                while (n > 0)
                {
                    if (n >= dec)
                    {
                        result.Append(letter[j]);
                        n -= dec;
                    }
                    else if (n >= dec - dec/10)
                    {
                        result.Append(letter[j + 2]);
                        result.Append(letter[j]);
                        n -= dec - dec/10;
                    }
                    else if (n >= dec/2)
                    {
                        result.Append(letter[j + 1]);
                        n -= dec/2;
                    }
                    else if (n >= dec/2 - dec/10)
                    {
                        result.Append(letter[j + 2]);
                        result.Append(letter[j + 1]);
                        n -= dec/2 - dec/10;
                    }
                    else if (dec == 10)
                    {
                        result.Append(letter[j + 2]);
                        n--;
                    }
                    else
                        break;
                }
            }
            if (form > 0)
            {
                result.Replace("XLV", "VL");
                result.Replace("XCV", "VC");
                result.Replace("CDL", "LD");
                result.Replace("CML", "LM");
                result.Replace("CMVC", "LMVL");
            }
            if (form == 1)
            {
                result.Replace ("CDXC", "LDXL");
                result.Replace ("CDVC", "LDVL");
                result.Replace ("CMXC", "LMXL");
                result.Replace ("XCIX", "VCIV");
                result.Replace ("XLIX", "VLIV");
            }
            if (form > 1)
            {
                result.Replace("XLIK", "IL");
                result.Replace("XCIX", "IC");
                result.Replace("CDXC", "XD");
                result.Replace("CDVC", "XDV");
                result.Replace("CDIC", "XDIX");
                result.Replace("LMVL", "XMV");
                result.Replace("CMIC", "XMIX");
                result.Replace("CMXC", "XM");
            }
            if (form > 2)
            {
                result.Replace("XDV", "VD");
                result.Replace("XDIX", "VDIV");
                result.Replace("XMV", "VM");
                result.Replace("XMIX", "VMIV");
            }
            if (form == 4)
            {
                result.Replace("VDIV", "ID");
                result.Replace("VMIV", "IM");
            }
            return result.ToString();
        }

        public static double Round([TreatNullsAsZero] double number, [Optional(0)] int num_digits)
        {
            if (num_digits < 0)
                return Round(number/10, num_digits + 1)*10;
            return Math.Round(number, num_digits);
        }

        public static double RoundDown([TreatNullsAsZero] double number, [Optional(0)] int num_digits)
        {
            if (Math.Abs(num_digits) > 100)
                throw new ErrorException(Errors.NumError);
            if (num_digits > 0)
                return RoundDown(number*10, num_digits - 1)/10;
            if (num_digits < 0)
                return RoundDown(number/10, num_digits + 1)*10;
            if (number < 0)
                return Math.Ceiling(number);
            return Math.Floor(number);
        }

        public static double RoundUp([TreatNullsAsZero] double number, [Optional(0)] int num_digits)
        {
            if (Math.Abs(num_digits) > 100)
                throw new ErrorException(Errors.NumError);
            if (num_digits > 0)
                return RoundUp(number*10, num_digits - 1)/10;
            if (num_digits < 0)
                return RoundUp(number/10, num_digits + 1)*10;
            if (number < 0)
                return Math.Floor(number);
            return Math.Ceiling(number);
        }

        public static double Sign([TreatNullsAsZero] double number)
        {
            return number.CompareTo(0.0d);
        }

        public static object Sqrt([TreatNullsAsZero] double number)
        {
            if (number < 0)
                return Errors.NumError;
            return Math.Sqrt(number);
        }

        public static object SubTotal(int function_num, IRange values)
        {
            function_num = function_num%100;
            switch (function_num)
            {
                case 1:
                    return
                        StatisticalFunctions.Average(
                            RangeToDoubleArrayConverterAttribute.RangeToDoubleArrayNoSubTotal(values));
                case 2:
                    return RangeToDoubleArrayConverterAttribute.RangeToDoubleArrayNoSubTotal(values).Length;
                case 3:
                    return RangeToDoubleArrayConverterAttribute.RangeToDoubleArrayNoSubTotal(values).Length; //\\
                case 4:
                    return
                        StatisticalFunctions.Max(
                            RangeToDoubleArrayConverterAttribute.RangeToDoubleArrayNoSubTotal(values));
                case 5:
                    return
                        StatisticalFunctions.Min(
                            RangeToDoubleArrayConverterAttribute.RangeToDoubleArrayNoSubTotal(values));
                case 6:
                    return Product(RangeToDoubleArrayConverterAttribute.RangeToDoubleArrayNoSubTotal(values));
                case 7:
                    return
                        StatisticalFunctions.StDev(
                            RangeToDoubleArrayConverterAttribute.RangeToDoubleArrayNoSubTotal(values));
                case 8:
                    return
                        StatisticalFunctions.StDevP(
                            RangeToDoubleArrayConverterAttribute.RangeToDoubleArrayNoSubTotal(values));
                case 9:
                    return Sum(RangeToDoubleArrayConverterAttribute.RangeToDoubleArrayNoSubTotal(values));
                case 10:
                    return
                        StatisticalFunctions.Var(
                            RangeToDoubleArrayConverterAttribute.RangeToDoubleArrayNoSubTotal(values));
                case 11:
                    return
                        StatisticalFunctions.VarP(
                            RangeToDoubleArrayConverterAttribute.RangeToDoubleArrayNoSubTotal(values));
                default:
                    return Errors.NaError;
            }
        }

        public static double Sum(
            [TreatBooleansAsDouble (ReferenceOption.ArgumentOnly), IgnoreNulls, AllowStringToDoubleConversion(ReferenceOption.ArgumentOnly, Strict=true),
             IgnoreStrings(ReferenceOption.ReferenceOnly),
            IgnoreBooleansAttribute(ReferenceOption.ReferenceOnly)] params double[] number)
        {
            double result = 0;
            foreach (double n in number)
                result += n;
            return result;
        }

        public static double SumIf([WorksheetContext ]IWorksheet worksheet , IRange range, string criteria, [Optional(null)] IRange sumRange)
        {
            double result = 0;
            Criteria cr = new Criteria(worksheet.Workbook, criteria);
            IRange targetRange = sumRange ?? range;
            RangeAndCriteria rangeAndCriteria = new RangeAndCriteria(worksheet.Workbook, range, criteria, targetRange);
            foreach (var c in rangeAndCriteria.GetDoubleMatches())
            {
                result += c;
            }
            return result;
        }

        
        public static object SumIfs([WorksheetContext] IWorksheet worksheet, IRange average_range,
                                        IRange criteria_range1, string criteria1,
                                        [NoConversion] params object[] additionalParameters)
        
        {
            if (additionalParameters.Length % 2 != 0)
                return Errors.ValueError;
            var rangeAndCriterias = new List<RangeAndCriteria>();
            rangeAndCriterias.Add(new RangeAndCriteria(worksheet.Workbook, criteria_range1, criteria1, average_range));
            for (int i = 0; i < additionalParameters.Length; )
            {
                var r = (IRange)additionalParameters[i];
                i++;
                object o = additionalParameters[i];
                if (o is ICell)
                    o = ((ICell)o).Value;
                rangeAndCriterias.Add(new RangeAndCriteria(worksheet.Workbook, r, o, average_range));
                i++;
            }
            var counts = new Dictionary<ICell, int>();
            foreach (RangeAndCriteria rangeandC in rangeAndCriterias)
            {
                foreach (ICell cell in rangeandC.GetMaches())
                {
                    if (!counts.ContainsKey(cell))
                        counts.Add(cell, 0);
                    counts[cell] = counts[cell] + 1;
                }
            }
            IParameterConverter converter = CompositeArgumentConverter.ToDoubleConverter();
            double result = 0;
            foreach (var pair in counts)
            {
                if (pair.Value == rangeAndCriterias.Count)
                {
                    object v = pair.Key.Value;
                    object res;
                    double? dbl = null;
                    if (v is double)
                        dbl = (double)v;
                    var cd = new ConversionData(average_range.Worksheet.Workbook, average_range.Worksheet, v,
                                                typeof(double));
                    converter.Convert(cd);
                    if (cd.Successful)
                    {
                        if (cd.Error != null)
                            return cd.Error;
                        dbl = (double)cd.Result;
                    }
                    result += dbl.Value;
                    
                }
            }
            return result;
        }

        public static object SumProduct(params IRange[] arrays)
        {
            if (arrays.Length == 0)
                return 0;
            if (!SameDimensions(arrays))
                return Errors.ValueError;
            double result = 0;
            IRange firstRange = arrays[0];
            for (int i = 0; i < firstRange.RowSpan; i++)
            {
                for (int j = 0; j < firstRange.ColumnSpan; j++)
                {
                    double temp = 1;
                    foreach (IRange r in arrays)
                    {
                        object v = r[i, j].Value;
                        if (v is double)
                            temp *= (double) v;
                        else
                        {
                            temp = 0;
                            break;
                        }
                    }
                    result += temp;
                }
            }
            return result;
        }

        private static bool SameDimensions(IRange[] arrays)
        {
            IRange firstRange = arrays[0];
            foreach (IRange r in arrays)
            {
                if (r.RowSpan != firstRange.RowSpan)
                    return false;
                if (r.ColumnSpan != firstRange.ColumnSpan)
                    return false;
            }
            return true;
        }

        public static double SumSq(
            [IgnoreBooleans, IgnoreNulls, AllowStringToDoubleConversion(ReferenceOption.ArgumentOnly, Strict=true),
             IgnoreStrings(ReferenceOption.ReferenceOnly)] params double[] number)
        {
            double result = 0;
            foreach (double d in number)
                result += d*d;
            return result;
        }

        public static double SumX2MY2([WorksheetContext]IWorksheet worksheet, IArray array_x, IArray array_y)
        {
            List<double> yList;
            List<double> xList;
            StatisticalFunctions.ExtractValues(array_x, array_y, out xList, out yList, worksheet.Workbook);
            int count = Math.Min(xList.Count, yList.Count);
            double result = 0;
            for (int i = 0; i < count; i++)
            {

                double x = xList[i];
                double y = yList[i];
                result += x*x - y*y;

            }
            return result;
        }

        public static double SumX2PY2([WorksheetContext]IWorksheet worksheet, IArray array_x, IArray array_y)
        {
            List<double> yList;
            List<double> xList;
            StatisticalFunctions.ExtractValues(array_x, array_y, out xList, out yList, worksheet.Workbook);
            int count = Math.Min(xList.Count, yList.Count);
            double result = 0;
            for (int i = 0; i < count; i++)
            {

                double x = xList[i];
                double y = yList[i];
                result += x*x + y*y;

            }
            return result;
        }

        public static double SumXMY2([WorksheetContext]IWorksheet worksheet, IArray array_x, IArray array_y)
        {
            List<double> yList;
            List<double> xList;
            StatisticalFunctions.ExtractValues(array_x, array_y, out xList, out yList, worksheet.Workbook);
            int count = Math.Min( xList.Count, yList.Count);
            double result = 0;
            for (int i = 0; i < count; i++ )
            {
                double x = xList[i];
                double y = yList[i];
                result += (x - y)*(x - y);
            }
            return result;
        }

        public static double Trunc([TreatNullsAsZero] double number, [Optional(0)] int num_digits)
        {
            if (number < 0)
                return -Trunc(-number, num_digits);

            double pow = Math.Pow(10, num_digits);
            return Math.Floor(number*pow)/pow;
        }


        public static double Cos([TreatNullsAsZero] double v)
        {
            return Math.Cos(v);
        }

        public static double Sin([TreatNullsAsZero] double value)
        {
            return Math.Sin(value);
        }

        public static double Sinh([TreatNullsAsZero] double value)
        {
            return Math.Sinh(value);
        }

        public static double Tan([TreatNullsAsZero] double value)
        {
            return Math.Tan(value);
        }

        public static double Tanh([TreatNullsAsZero] double value)
        {
            return Math.Tanh(value);
        }
    }

    internal sealed class FuncArray : IArray
    {
        private readonly Func<int, int, object> _func;
        private readonly int _rowCount;
        private readonly int _columnCount;

        public FuncArray(Func<int, int, object>func, int rowCount, int columnCount)
        {
            _func = func;
            _rowCount = rowCount;
            _columnCount = columnCount;
        }

        public IEnumerator<object> GetEnumerator()
        {
            for (int i = 0; i < RowCount; i++)
                for (int j = 0; j < ColumnCount; j++)
                    yield return this[i, j];
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        public object this[int row, int column]
        {
            get { return _func(row, column); }
        }

        public int RowCount
        {
            get { return _rowCount;}
        }

        public int ColumnCount
        {
            get { return _columnCount; }
        }
    }
}